package live.sidian.database.synchronizer.sync.structure;

import cn.hutool.core.util.StrUtil;
import live.sidian.database.synchronizer.infrastructure.sql.SqlCreator;
import live.sidian.database.synchronizer.infrastructure.utils.BeanUtil;
import live.sidian.database.synchronizer.infrastructure.utils.CollUtil;
import live.sidian.database.synchronizer.sync.structure.model.Column;
import live.sidian.database.synchronizer.sync.structure.model.Index;
import live.sidian.database.synchronizer.sync.structure.model.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.*;
import java.util.function.Function;

import static live.sidian.database.synchronizer.sync.structure.MetaDataComparator.ACTION.*;
import static live.sidian.database.synchronizer.sync.structure.model.Index.IndexType.PRIMARY;
import static live.sidian.database.synchronizer.sync.structure.model.Index.IndexType.UNIQUE;

/**
 * 元数据差异比较器
 *
 * @author sidian
 * @date 2021/4/4 14:29
 */
public class MetaDataComparator {
    /**
     * 操作类型
     */
    enum ACTION {
        ADD_DB, UPDATE_DB, DELETE_DB, USE_DB,
        ADD_TABLE, UPDATE_TABLE, DELETE_TABLE,
        ADD_COLUMN, UPDATE_COLUMN, DELETE_COLUMN,
        ADD_INDEX, UPDATE_INDEX, DELETE_INDEX,
        ADD_AUTO_INC, DELETE_AUTO_INC
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class DiffChange {
        ACTION action;
        String val;
    }


    public static class Diff {
        List<DiffChange> changes = new LinkedList<>();

        public void add(ACTION action, String val) {
            changes.add(new DiffChange(action, val));
        }

        public void add(ACTION action, List<String> vals) {
            vals.forEach(val -> add(action, val));
        }

        public void add(Diff diff) {
            changes.addAll(diff.changes);
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            changes.forEach(diffChange -> builder.append(diffChange.val).append("\n\n"));
            return builder.toString();
        }
    }


    /**
     * diff database
     */
    public static Diff diff(MetaData source, MetaData target) {
        Diff diff = new Diff();
        // 都不存在
        if (!source.exist && !target.exist) {
            return diff;
        }
        // source数据库不存在
        if (!source.exist) {
            diff.add(DELETE_DB, SqlCreator.dropDatabase(target.getSchema()));
            return diff;
        }
        // target数据库不存在
        if (!target.exist) {
            diff.add(ADD_DB, SqlCreator.createDatabase(target.getSchema(), source.getCharset()));
            diff.add(USE_DB, SqlCreator.useDb(target.getSchema()));
            source.tables.forEach(table -> diff.add(ADD_TABLE, table.getCreateSql()));
            return diff;
        }
        // 字符编码不一致
        if (!Objects.equals(source.getCharset(), target.getCharset())) {
            diff.add(UPDATE_DB, source.getCharset());
        }
        // diff table
        diff.add(diffTable(source.getTables(), target.getTables(), target.getSchema()));
        return diff;
    }

    /**
     * diff tables
     */
    private static Diff diffTable(List<Table> sources, List<Table> targets, String schema) {
        Diff diff = new Diff();
        // 集合操作
        Collection<Table> intersection = CollUtil.intersection(sources, targets, Table::getName);
        Collection<Table> sourceSubtract = CollUtil.subtract(sources, intersection, Table::getName);
        Collection<Table> targetSubtract = CollUtil.subtract(targets, intersection, Table::getName);

        // sources多出来的需要补充
        sourceSubtract.forEach(table -> diff.add(ADD_TABLE, table.getCreateSql()));
        // target多出来的需要删除
        targetSubtract.forEach(table -> diff.add(DELETE_TABLE, SqlCreator.dropTable(table.getSchema(), table.getName())));
        // 交集需要深度diff
        Map<String, Table> sourceMap = CollUtil.toMap(sources, Table::getName);
        Map<String, Table> targetMap = CollUtil.toMap(targets, Table::getName);
        intersection.forEach(table -> {
            // diff 列和索引
            Table source = sourceMap.get(table.getName());
            Table target = targetMap.get(table.getName());
            diff.add(diffColumns(source.getColumns(), target.getColumns(), target.getSchema()));
            diff.add(diffIndexes(source.getIndexes(), target.getIndexes(), target.getSchema()));
        });
        // 特殊约束导致的顺序调整: auto_increment
        reOrder(diff);
        // 执行操作前, 需要选中数据库
        if (!diff.changes.isEmpty()) {
            ((LinkedList<DiffChange>) diff.changes).addFirst(new DiffChange(USE_DB, SqlCreator.useDb(schema)));
        }
        return diff;
    }

    /**
     * 操作顺序调整(仅用于表的字段与索引排序)<br/>
     * auto_increment删 -> 索引删 -> 列删 -> 列增 -> 索引增 -> auto_increment增
     */
    private static void reOrder(Diff diff) {
        HashMap<ACTION, Integer> actionMap = new HashMap<ACTION, Integer>() {{
            put(DELETE_AUTO_INC, 0);
            put(DELETE_INDEX, 1);
            put(DELETE_COLUMN, 2);
            put(ADD_COLUMN, 3);
            put(ADD_INDEX, 4);
            put(ADD_AUTO_INC, 5);
        }};
        diff.changes.sort(Comparator.comparingInt(value -> actionMap.getOrDefault(value.action, -1)));
    }


    private static Diff diffColumns(List<Column> sources, List<Column> targets, String schema) {
        Diff diff = new Diff();
        // 集合操作
        Collection<Column> intersection = CollUtil.intersection(sources, targets, Column::getName);
        Collection<Column> sourceSubtract = CollUtil.subtract(sources, intersection, Column::getName);
        Collection<Column> targetSubtract = CollUtil.subtract(targets, intersection, Column::getName);

        // target多出来的需要删除
        targetSubtract.forEach(column -> diff.add(DELETE_COLUMN, SqlCreator.dropColumn(column.getSchema(), column.getTable(), column.getName())));
        // source多出来的
        sourceSubtract.forEach(column -> diff.add(addColumn(column, schema)));
        // 交集
        Map<String, Column> sourceMap = CollUtil.toMap(sources, Column::getName);
        Map<String, Column> targetMap = CollUtil.toMap(targets, Column::getName);
        intersection.stream()
                .filter(column -> !Objects.equals(sourceMap.get(column.getName()), targetMap.get(column.getName())))
                .forEach(column -> diff.add(updateColumn(sourceMap.get(column.getName()), targetMap.get(column.getName()))));
        return diff;
    }

    private static final Function<Column, String> columnDefinitionGetter = column -> SqlCreator.buildColumnDefinition(
            column.getType(), column.isNullable(), column.getExtra(), column.getDefaultValue(), column.getComment());


    private static Diff updateColumn(Column source, Column target) {
        Diff diff = new Diff();
        Column column = BeanUtil.copyProperties(source, Column.class);
        column.setSchema(target.getSchema());
        if (StrUtil.isNotBlank(source.getExtra()) && StrUtil.isBlank(target.getExtra())) { // 修改后, 有auto_increment约束
            Column t = BeanUtil.copyProperties(column, Column.class);
            t.setExtra(null);
            diff.add(UPDATE_COLUMN, doUpdateColumn(t));
            diff.add(ADD_AUTO_INC, doUpdateColumn(column));
        } else if (StrUtil.isBlank(source.getExtra()) && StrUtil.isNotBlank(target.getExtra())) { // 修改后, 无auto_increment约束
            diff.add(DELETE_AUTO_INC, doUpdateColumn(column));
        } else { // auto_increment约束不变
            diff.add(UPDATE_COLUMN, doUpdateColumn(column));
        }
        return diff;
    }

    private static String doUpdateColumn(Column column) {
        return SqlCreator.modifyColumn(column.getSchema(), column.getTable(), column.getName(), columnDefinitionGetter.apply(column), column.getAfter());
    }

    private static Diff addColumn(Column column, String schema) {
        Diff diff = new Diff();
        Column _column = BeanUtil.copyProperties(column, Column.class);
        _column.setSchema(schema);
        if (StrUtil.isNotBlank(_column.getExtra())) { // 有auto_increment约束
            Column t = BeanUtil.copyProperties(_column, Column.class);
            t.setExtra(null);
            diff.add(ADD_COLUMN, doAddColumn(t));
            diff.add(ADD_AUTO_INC, doAddColumn(_column));
        } else { // 无
            diff.add(ADD_COLUMN, doAddColumn(_column));
        }
        return diff;
    }

    private static String doAddColumn(Column column) {
        return SqlCreator.addColumn(column.getSchema(), column.getTable(), column.getName(), columnDefinitionGetter.apply(column), column.getAfter());
    }

    private static Diff diffIndexes(List<Index> sources, List<Index> targets, String schema) {
        Diff diff = new Diff();
        // 集合操作
        Collection<Index> intersection = CollUtil.intersection(sources, targets, Index::getName);
        Collection<Index> sourceSubtract = CollUtil.subtract(sources, targets, Index::getName);
        Collection<Index> targetSubtract = CollUtil.subtract(targets, sources, Index::getName);
        // target多余的
        targetSubtract.forEach(index -> diff.add(DELETE_INDEX, removeIndex(index)));
        // source多余的
        sourceSubtract.forEach(index -> {
            Index t = BeanUtil.copyProperties(index, Index.class);
            t.setSchema(schema);
            diff.add(ADD_INDEX, addIndex(t));
        });
        // 交集
        Map<String, Index> sourceMap = CollUtil.toMap(sources, Index::getName);
        Map<String, Index> targetMap = CollUtil.toMap(targets, Index::getName);
        intersection.stream()
                .filter(index -> !Objects.equals(sourceMap.get(index.getName()), targetMap.get(index.getName())))
                .forEach(index -> diff.add(updateIndex(sourceMap.get(index.getName()), targetMap.get(index.getName()))));
        return diff;
    }

    private static Diff updateIndex(Index source, Index target) {
        Diff diff = new Diff();
        // 删除老的
        diff.add(DELETE_INDEX, removeIndex(target));
        // 添加新的
        Index index = BeanUtil.copyProperties(source, Index.class);
        index.setSchema(target.getSchema());
        diff.add(ADD_INDEX, addIndex(index));
        return diff;
    }

    private static String addIndex(Index index) {
        return index.getType() == PRIMARY ?
                SqlCreator.addPrimaryKey(index.getSchema(), index.getTable(), index.getColumns())
                : SqlCreator.addIndex(index.getSchema(), index.getTable(), index.getName(), index.getColumns(), index.getType() == UNIQUE);
    }

    private static String removeIndex(Index index) {
        return index.getType() == PRIMARY ?
                SqlCreator.dropPrimaryKey(index.getSchema(), index.getTable())
                : SqlCreator.dropIndex(index.getSchema(), index.getTable(), index.getName());
    }

}
